@dashincubator/secp256k1
Browser, Node, and bundler compatible release of
@noble/secp256k1.
Fastest JS implementation of
secp256k1, an elliptic curve that could be
used for asymmetric encryption, ECDH key agreement protocol and signature
schemes. Supports deterministic ECDSA from RFC6979 and Schnorr
signatures from
BIP0340.
Audited by an independent security firm. Check out
the online demo and blog post:
Learning fast elliptic-curve cryptography in JS
This fork is a standalone browser-compatible release of noble crypto
noble-crypto — high-security, easily auditable set of contained
cryptographic libraries and tools.
Install
Node, Bun, & Bundlers
npm install --save @dashincubator/secp256k1
CommonJS and ECMAScript Modules (ESM)
var Secp256k1 = require("@dashincubator/secp256k1");
import * as Secp256k1 from "@dashincubator/secp256k1";
Browsers
<script src="https://unpkg.com/@dashincubator/secp256k1@1.7.0/secp256k1.js"></script>
<script>
let Secp256k1 = window.nobleSecp256k1;
</script>
Usage
Use NPM in node.js / browser, or include single file from
GitHub's releases page:
(async () => {
const privKey = Secp256k1.utils.randomPrivateKey();
const pubKey = Secp256k1.getPublicKey(privKey);
const msgHash = await Secp256k1.utils.sha256("hello world");
const signature = await Secp256k1.sign(msgHash, privKey);
const isValid = Secp256k1.verify(signature, msgHash, pubKey);
const rpub = Secp256k1.schnorr.getPublicKey(privKey);
const rsignature = await Secp256k1.schnorr.sign(message, privKey);
const risValid = await Secp256k1.schnorr.verify(rsignature, message, rpub);
})();
To use the module with Deno, you will need
import map:
deno run --import-map=imports.json app.ts
- app.ts:
import * as Secp256k1 from "https://deno.land/x/secp256k1/mod.ts";
- imports.json:
{"imports": {"crypto": "https://deno.land/std@0.153.0/node/crypto.ts"}}
API
getPublicKey(privateKey)
function getPublicKey(
privateKey: Uint8Array | string | bigint,
isCompressed = false,
): Uint8Array;
Creates public key for the corresponding private key. The default is full
65-byte key.
isCompressed = false
determines whether to return compact (33-byte), or full
(65-byte) key.
Internally, it does Point.BASE.multiply(privateKey)
. If you need actual
Point
instead of Uint8Array
, use Point.fromPrivateKey(privateKey)
.
sign(msgHash, privateKey)
function sign(
msgHash: Uint8Array | string,
privateKey: Uint8Array | string,
opts?: Options,
): Promise<Uint8Array>;
function sign(
msgHash: Uint8Array | string,
privateKey: Uint8Array | string,
opts?: Options,
): Promise<[Uint8Array, number]>;
Generates low-s deterministic ECDSA signature as per RFC6979.
msgHash: Uint8Array | string
- 32-byte message hash which would be signedprivateKey: Uint8Array | string | bigint
- private key which will sign the
hashoptions?: Options
- optional object related to signature value and format
with following keys:
recovered: boolean = false
- whether the recovered bit should be included
in the result. In this case, the result would be an array of two items.canonical: boolean = true
- whether a signature s
should be no more than
1/2 prime order. true
(default) makes signatures compatible with
libsecp256k1, false
makes signatures compatible with opensslder: boolean = true
- whether the returned signature should be in DER
format. If false
, it would be in Compact format (32-byte r + 32-byte s)extraEntropy: Uint8Array | string | true
- additional entropy k'
for
deterministic signature, follows section 3.6 of RFC6979. When true
, it
would automatically be filled with 32 bytes of cryptographically secure
entropy. Strongly recommended to pass true
to improve security:
- Schnorr signatures are doing it every time
- It would help a lot in case there is an error somewhere in
k
generation.
Exposing k
could leak private keys - If the entropy generator is broken, signatures would be the same as they
are without the option
- Signatures with extra entropy would have different
r
/ s
, which means
they would still be valid, but may break some test vectors if you're
cross-testing against other libs
The function is asynchronous because we're utilizing built-in HMAC API to not
rely on dependencies.
(async () => {
const signatureE = await Secp256k1.sign(msgHash, privKey, {
extraEntropy: true,
});
const signatureM = await Secp256k1.sign(msgHash, privKey, {
canonical: false,
});
})();
function signSync(
msgHash: Uint8Array | string,
privateKey: Uint8Array | string,
opts?: Options,
): Uint8Array | [Uint8Array, number];
signSync
counterpart could also be used, you need to set
utils.hmacSha256Sync
to a function with signature
key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array
. Example with
noble-hashes
package:
import { hmac } from "@noble/hashes/hmac";
import { sha256 } from "@noble/hashes/sha256";
secp256k1.utils.hmacSha256Sync = (key, ...msgs) =>
hmac(sha256, key, secp256k1.utils.concatBytes(...msgs));
secp256k1.utils.sha256Sync = (...msgs) =>
sha256(secp256k1.utils.concatBytes(...msgs));
secp256k1.signSync(msgHash, privateKey);
schnorr.signSync(message, privateKey);
verify(signature, msgHash, publicKey)
function verify(
signature: Uint8Array | string,
msgHash: Uint8Array | string,
publicKey: Uint8Array | string,
): boolean;
function verify(
signature: Signature,
msgHash: Uint8Array | string,
publicKey: Point,
): boolean;
signature: Uint8Array | string | { r: bigint, s: bigint }
- object returned
by the sign
functionmsgHash: Uint8Array | string
- message hash that needs to be verifiedpublicKey: Uint8Array | string | Point
- e.g. that was generated from
privateKey
by getPublicKey
options?: Options
- optional object related to signature value and format
strict: boolean = true
- whether a signature s
should be no more than
1/2 prime order. true
(default) makes signatures compatible with
libsecp256k1, false
makes signatures compatible with openssl
- Returns
boolean
: true
if signature == hash
; otherwise false
getSharedSecret(privateKeyA, publicKeyB)
function getSharedSecret(
privateKeyA: Uint8Array | string | bigint,
publicKeyB: Uint8Array | string | Point,
isCompressed = false,
): Uint8Array;
Computes ECDH (Elliptic Curve Diffie-Hellman) shared secret between a private
key and a different public key.
-
To get Point instance, use Point.fromHex(publicKeyB).multiply(privateKeyA)
-
isCompressed = false
determines whether to return compact (33-byte), or full
(65-byte) key
-
If you have one public key you'll be creating lots of secrets against,
consider massive speed-up by using precomputations:
const pub = Secp256k1.utils.precompute(8, publicKeyB);
getSharedSecret(privKey, pub);
recoverPublicKey(hash, signature, recovery)
function recoverPublicKey(
msgHash: Uint8Array | string,
signature: Uint8Array | string,
recovery: number,
isCompressed = false,
): Uint8Array | undefined;
Recovers public key from message hash, signature & recovery bit. The default is
full 65-byte key.
msgHash: Uint8Array | string
- message hash which would be signedsignature: Uint8Array | string | { r: bigint, s: bigint }
- object returned
by the sign
functionrecovery: number
- recovery bit returned by sign
with recovered
optionisCompressed = false
determines whether to return compact (33-byte), or full
(65-byte) key
Public key is generated by doing scalar multiplication of a base Point(x, y) by
a fixed integer. The result is another Point(x, y)
which we will by default
encode to hex Uint8Array. If signature is invalid - function will return
undefined
as result. To get Point instance, use
Point.fromSignature(hash, signature, recovery)
.
schnorr.getPublicKey(privateKey)
function schnorrGetPublicKey(privateKey: Uint8Array | string): Uint8Array;
Calculates 32-byte public key from a private key.
Warning: it is incompatible with non-schnorr pubkey. Specifically, its y
coordinate may be flipped. See
BIP340 for
clarification.
schnorr.sign(message, privateKey)
function schnorrSign(
message: Uint8Array | string,
privateKey: Uint8Array | string,
auxilaryRandom?: Uint8Array,
): Promise<Uint8Array>;
Generates Schnorr signature as per BIP0340. Asynchronous, so use await
.
message: Uint8Array | string
- message (not hash) which would be signedprivateKey: Uint8Array | string | bigint
- private key which will sign the
hashauxilaryRandom?: Uint8Array
— optional 32 random bytes. By default, the
method gathers cryptogarphically secure entropy- Returns Schnorr signature in Hex format.
schnorr.verify(signature, message, publicKey)
function schnorrVerify(
signature: Uint8Array | string,
message: Uint8Array | string,
publicKey: Uint8Array | string,
): boolean;
signature: Uint8Array | string | { r: bigint, s: bigint }
- object returned
by the sign
functionmessage: Uint8Array | string
- message (not hash) that needs to be verifiedpublicKey: Uint8Array | string | Point
- e.g. that was generated from
privateKey
by getPublicKey
- Returns
boolean
: true
if signature == hash
; otherwise false
Utilities
secp256k1 exposes a few internal utilities for improved developer experience.
let hex = Secp256k1.utils.bytesToHex(pubKey);
console.log(hex);
const utils: {
hashToPrivateKey: (hash: Hex) => Uint8Array;
randomPrivateKey: () => Uint8Array;
isValidPrivateKey(privateKey: PrivKey): boolean;
randomBytes: (bytesLength?: number) => Uint8Array;
bytesToHex(uint8a: Uint8Array): string;
hexToBytes(hex: string): Uint8Array;
concatBytes(...arrays: Uint8Array[]): Uint8Array;
mod: (number: number | bigint, modulo = CURVE.P): bigint;
invert(number: bigint, modulo?: bigint): bigint;
sha256: (message: Uint8Array) => Promise<Uint8Array>;
hmacSha256: (key: Uint8Array, ...messages: Uint8Array[]) => Promise<Uint8Array>;
sha256Sync: undefined;
hmacSha256Sync: undefined;
taggedHash: (tag: string, ...messages: Uint8Array[]) => Promise<Uint8Array>;
taggedHashSync: (tag: string, ...messages: Uint8Array[]) => Uint8Array;
precompute(windowSize?: number, point?: Point): Point;
};
secp256k1.CURVE.P
secp256k1.CURVE.n
secp256k1.Point.BASE
secp256k1.Point {
constructor(x: bigint, y: bigint);
static fromHex(hex: Uint8Array | string);
static fromPrivateKey(privateKey: Uint8Array | string | number | bigint);
static fromSignature(
msgHash: Hex,
signature: Signature,
recovery: number | bigint
): Point | undefined {
toRawBytes(isCompressed = false): Uint8Array;
toHex(isCompressed = false): string;
equals(other: Point): boolean;
negate(): Point;
add(other: Point): Point;
subtract(other: Point): Point;
multiply(scalar: bigint | Uint8Array): Point;
}
secp256k1.Signature {
constructor(r: bigint, s: bigint);
static fromDER(hex: Uint8Array | string);
static fromCompact(hex: Uint8Array | string);
assertValidity(): void;
hasHighS(): boolean;
toDERRawBytes(): Uint8Array;
toDERHex(): string;
toCompactRawBytes(): Uint8Array;
toCompactHex(): string;
}
Security
Noble is production-ready.
- The library has been audited by an independent security firm cure53:
PDF. See
changes since audit.
- The library has also been fuzzed by
Guido Vranken's cryptofuzz. You
can run the fuzzer by yourself to check it.
We're using built-in JS BigInt
, which is potentially vulnerable to
timing attacks as
per official spec.
But, JIT-compiler and Garbage Collector make "constant time" extremely hard
to achieve in a scripting language. Which means any other JS library doesn't
use constant-time bigints. Including bn.js or anything else. Even statically
typed Rust, a language without GC,
makes it harder to achieve constant-time
for some cases. If your goal is absolute security, don't use any JS lib —
including bindings to native ones. Use low-level libraries & languages.
Nonetheless we've hardened implementation of ec curve multiplication to be
algorithmically constant time.
We however consider infrastructure attacks like rogue NPM modules very
important; that's why it's crucial to minimize the amount of 3rd-party
dependencies & native bindings. If your app uses 500 dependencies, any dep could
get hacked and you'll be downloading malware with every npm install
. Our goal
is to minimize this attack vector.
Speed
Benchmarks measured with Apple M2 on MacOS 12 with node.js 18.8.
getPublicKey(utils.randomPrivateKey()) x 7,093 ops/sec @ 140μs/op
sign x 5,615 ops/sec @ 178μs/op
signSync (@noble/hashes) x 5,209 ops/sec @ 191μs/op
verify x 1,114 ops/sec @ 896μs/op
recoverPublicKey x 1,018 ops/sec @ 982μs/op
getSharedSecret aka ecdh x 665 ops/sec @ 1ms/op
getSharedSecret (precomputed) x 7,426 ops/sec @ 134μs/op
Point.fromHex (decompression) x 14,582 ops/sec @ 68μs/op
schnorr.sign x 805 ops/sec @ 1ms/op
schnorr.verify x 1,129 ops/sec @ 885μs/op
Compare to other libraries on M1 (openssl
uses native bindings, not JS):
elliptic#getPublicKey x 1,940 ops/sec
sjcl#getPublicKey x 211 ops/sec
elliptic#sign x 1,808 ops/sec
sjcl#sign x 199 ops/sec
openssl#sign x 4,243 ops/sec
ecdsa#sign x 116 ops/sec
bip-schnorr#sign x 60 ops/sec
elliptic#verify x 812 ops/sec
sjcl#verify x 166 ops/sec
openssl#verify x 4,452 ops/sec
ecdsa#verify x 80 ops/sec
bip-schnorr#verify x 56 ops/sec
elliptic#ecdh x 971 ops/sec
Contributing
Check out a blog post about this library:
Learning fast elliptic-curve cryptography in JS.
- Clone the repository.
npm install
to install build dependencies like TypeScriptnpm run build
to compile TypeScript codenpm test
to run jest on test/index.ts
Special thanks to Roman Koblov, who have
helped to improve scalar multiplication speed.
License
MIT (c) Paul Miller (https://paulmillr.com), see
LICENSE file.